package client;

import android.content.Context;

import com.example.myapplication.MainActivity;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.webrtc.DataChannel;
import org.webrtc.DefaultVideoDecoderFactory;
import org.webrtc.DefaultVideoEncoderFactory;
import org.webrtc.EglBase;
import org.webrtc.MediaConstraints;
import org.webrtc.PeerConnection;
import org.webrtc.PeerConnectionFactory;
import org.webrtc.SessionDescription;
import org.webrtc.VideoDecoderFactory;
import org.webrtc.VideoEncoderFactory;

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;

import json_object_mapper.Trickle;
import observer.SessionObserver;
import webrtcwrappers.PeerConnectionCreator;
import signal.Role;
import signal.Signal;
import signal.Transport;
import stream.LocalStream;
import utils.WebRTCUtils;
import webrtcwrappers.IceCandidateInit;
public class Client {

    private static final Logger logger = LoggerFactory.getLogger(Client.class);

    private Map<Role, Transport> transports;
    private final Configuration configuration;
    private final Signal signal;
    private final PeerConnectionFactory factory;
    private final MainActivity context;

    public Client(Signal signal, Configuration configuration, MainActivity context) {
        this.signal = signal;
        this.factory = getPeerConnectionFactory(context);
        if (configuration == null) {
            this.configuration = new Configuration(WebRTCUtils.getInitIceServers());
            this.configuration.setCodec(CodecType.VP8);

        } else {
            this.configuration = configuration;
        }
        this.signal.setOnTrickle(this::trickle);
        this.signal.setOnNegotiate(this::negotiate);
        this.context = context;
    }

    public static PeerConnectionFactory getPeerConnectionFactory(Context context) {
        PeerConnectionFactory.InitializationOptions initializationOptions =
                PeerConnectionFactory.InitializationOptions.builder(context).createInitializationOptions();
        EglBase.Context eglContext = EglBase.create().getEglBaseContext();
        VideoDecoderFactory videoDecoder = new DefaultVideoDecoderFactory(eglContext);
        VideoEncoderFactory videoEncoder = new DefaultVideoEncoderFactory(eglContext, true, true);
        PeerConnectionFactory.initialize(initializationOptions);
        return PeerConnectionFactory.builder()
                .setVideoDecoderFactory(videoDecoder)
                .setVideoEncoderFactory(videoEncoder)
                .createPeerConnectionFactory();
    }


    public void sendMessage(DataChannel channel, String message) {
        ByteBuffer byteBuffer = ByteBuffer.wrap(message.getBytes(StandardCharsets.UTF_8));
        DataChannel.Buffer buffer = new DataChannel.Buffer(byteBuffer, false);
        if (channel != null) {
            channel.send(buffer);
        }
    }

    public void  join(String sid, String uid) {
        try {
            this.transports = createPairPC(this.signal, this.configuration);
            Transport pubTransport = this.transports.get(Role.PUBLISHER);
            assert pubTransport != null;
            PeerConnection pubPC = pubTransport.getPeerConnectionCreator().getPc();
            MediaConstraints mediaConstraints = new MediaConstraints();
            SessionObserver observer = new SessionObserver();
            pubPC.createOffer(observer, mediaConstraints);
            SessionDescription offer = observer.getSdp();
            pubPC.setLocalDescription(new SessionObserver(), offer);
            SessionDescription answer = this.signal.join(sid, uid, offer).get();
            pubPC.setRemoteDescription(new SessionObserver(), answer);
            pubTransport.getCandidates().forEach(candidateInit -> pubPC
                    .addIceCandidate(IceCandidateInit.convertToIceCandidate(candidateInit)));
        } catch (ExecutionException | InterruptedException e) {
            e.printStackTrace();
        }
    }


    public void publish(LocalStream stream) {
        if (this.transports == null) {
            throw new IllegalStateException();
        }
        Transport pubTransport = this.transports.get(Role.PUBLISHER);
        assert pubTransport != null;
        stream.publish(pubTransport.getPeerConnectionCreator().getPc());
        pubTransport.getPeerConnectionCreator().renegotiate();
    }

    public void close() {
        if (this.transports != null) {
            this.transports.forEach((role, transport) -> transport.getPeerConnectionCreator().getPc().close());
        }
        this.signal.close();
    }


    private void negotiate(SessionDescription sdp) {
        logger.info("got an offer from SFU: {}", sdp.description);
        if (this.transports == null) {
            throw new IllegalStateException();
        }
        Transport transport = this.transports.get(Role.SUBSCRIBER);
        SessionObserver observer = new SessionObserver();
        assert transport != null;
        transport.getPeerConnectionCreator().getPc().setRemoteDescription(observer, sdp);
        transport.getCandidates().forEach(iceCandidateInit -> transport.getPeerConnectionCreator().getPc()
                .addIceCandidate(IceCandidateInit.convertToIceCandidate(iceCandidateInit)));
        transport.setCandidates(new ArrayList<>());
        transport.getPeerConnectionCreator().getPc().createAnswer(observer, new MediaConstraints());
        SessionDescription answer = observer.getSdp();
        transport.getPeerConnectionCreator().getPc().setLocalDescription(observer, answer);
        logger.info("Send answer to SFU for subscriber: {}", answer.description);
        this.signal.answer(answer);
    }

    private void trickle(Trickle trickle) {
        int role = trickle.getTarget();
        IceCandidateInit candidate = trickle.getCandidate();
        if (this.transports == null) {
            throw new IllegalStateException();
        }
        final Role ROLE;
        if (role == 0) {
            ROLE = Role.PUBLISHER;
        } else {
            ROLE = Role.SUBSCRIBER;
        }
        Transport transport = transports.get(ROLE);
        if (transport.getPeerConnectionCreator().getPc().getRemoteDescription() != null) {
            Objects.requireNonNull(this.transports.get(ROLE)).getPeerConnectionCreator().getPc()
                    .addIceCandidate(IceCandidateInit.convertToIceCandidate(candidate));
        } else {
            Objects.requireNonNull(this.transports.get(ROLE)).getCandidates().add(candidate);
        }
    }



    private Map<Role, Transport> createPairPC(Signal signal, Configuration configuration) {
        Map<Role, Transport> pairPeerConnection = new ConcurrentHashMap<>();
        pairPeerConnection.put(Role.PUBLISHER, new Transport(Role.PUBLISHER, signal, configuration, factory));
        pairPeerConnection.put(Role.SUBSCRIBER, new Transport(Role.SUBSCRIBER, signal, configuration, factory));
        PeerConnectionCreator subscriberPeer = pairPeerConnection.get(Role.SUBSCRIBER).getPeerConnectionCreator();
        subscriberPeer.setContext(context);
        return pairPeerConnection;
    }

    public Map<Role, Transport> getTransports() {
        return this.transports;
    }
}

enum CodecType {
    VP8,
    VP9,
    H264
}

class Configuration extends PeerConnection.RTCConfiguration {
    private CodecType codec;

    public Configuration(List<PeerConnection.IceServer> iceServers) {
        super(iceServers);
        super.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN;
    }

    public void setCodec(CodecType codec) {
        this.codec = codec;
    }

    public CodecType getCodec() {
        return codec;
    }

}





